---
title: Queues
description: Messages, queues, and background tasks
---

import { Aside } from "@astrojs/starlight/components";

Whilst building a webapp you want to be able to respond as quickly as possible to user interactions, but sometimes you need to do things that take a long time! For example, you might need to send an email when a user submits a form, or you need to process a payment, or do something magic with AI - and you don't want the user to wait for these things to complete.

To handle these you'll use **background tasks**. Background tasks are managed by the [Cloudflare queue system](https://developers.cloudflare.com/queues/). You send a message to a queue, where a worker will process the message, but on a different worker - so it doesn't block the main one.

<Aside type="tip" title="Pick your poison">
  You do not need to use Cloudflare's queue system, you can use any background task system you want, but we recommend
  using queues!
</Aside>

### Setup

First thing you've got to do is create a queue and bind the queue producers and consumers to your worker.

```bash showLineNumbers=false
npx wrangler queues create my-queue-name
```

Replace `my-queue-name` with the name of your queue, and place the following in your `wrangler.jsonc` file:

```jsonc title="wrangler.jsonc"
{
  "queues": {
    "producers": [
      {
        "binding": "QUEUE",
        "queue": "my-queue-name",
      }
    ],
    "consumers": [
      {
        "queue": "my-queue-name",
        "max_batch_size": 10,
        "max_batch_timeout": 5
      }
    ]
  }
}
```

After updating `wrangler.jsonc`, run `pnpm generate` to update the generated type definitions.

This will bind the queue to the `env.QUEUE` object in the worker. So you'll be able to send messages.


#### Naming Queues

Queue names must match the following RegEx pattern: `^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`

##### Valid queue names

- `my-queue`
- `my-awesome-queue-123`
- `queue1`
- `1queue`
- `my-queue-v2`

##### Invalid queue names

- `My_Queue` (uppercase letters not allowed)
- `MY_QUENE_NAME` (uppercase letters and underscores not allowed))
- `-queue-` (cannot start or end with hyphen)
- `really-really-really-really-really-really-really-long-queue-name` (max 63 chars)
- `queue_name` (underscores not allowed)

### Sending messages

```tsx title="src/worker.tsx"
import { env } from "cloudflare:workers";

export default defineApp([
  route('/pay-with-ai', () => {
    // Post a message to the queue
    env.QUEUE.send({
      userId: 1,
      amount: 100,
      currency: 'USD',
    })

    return new Response('Done!')
  })
])
```

### Receiving messages

In order to "consume messages" from the queue you need to change the shape of the `default` export of your worker. You'll add another function called `queue` that will receive a batch of messages.

```tsx title="src/worker.tsx" mark={5-9}

const app = defineApp([ /* routes... */])

export default {
  fetch: app.fetch,
  async queue(batch) {
    for (const message of batch.messages) {
      console.log('handling message' + JSON.stringify(message))
    }
  }
} satisfies ExportedHandler<Env>;
```

This will receive a batch of messages, and process them one by one.


## Ways to Send Messages

Cloudflare Queues allow Workers to send and process asynchronous messages reliably.

There are three common approaches to send data:

### Send Message Body Directly (up to 128KB)

Best for: Small payloads that fit within the 128KB limit.

```ts
await queue.send({
  body: JSON.stringify({ email: "user@example.com", subject: "Welcome!" }),
});
```
✅ Simple and fast
❌ Hard limit of 128KB per message


### Store in R2 and Send Object Key


Best for: Large payloads (e.g., files, JSON blobs, videos).

```ts
// Upload to R2 first
await r2.put("msg/123.json", JSON.stringify(largeData));

// Then send only the key to the queue
await queue.send({
  body: JSON.stringify({ r2Key: "msg/123.json" }),
});
```

✅ Great for large data
✅ Persistent and versioned if needed
❌ Slightly more complex (requires R2 integration)

### Store in KV and Send KV Key

Best for: Short-lived messages or small-to-medium payloads.

```
// Save to KV
await kv.put("queue:msg:123", JSON.stringify(data), { expirationTtl: 600 });

// Send reference key
await queue.send({
  body: JSON.stringify({ kvKey: "queue:msg:123" }),
});
```
✅ Fast access
✅ Automatic expiration possible
❌ Not ideal for large data
❌ KV has eventual consistency (meaning that when you write data to Cloudflare KV (via kv.put), it might not be immediately visible to all readers — especially in different Cloudflare data centers.)

### Tips

#### Handling Different Queues

By sending a message on a queue

```tsx title="src/worker.tsx"
import { env } from "cloudflare:workers";

export default defineApp([
  route('/pay-with-ai', () => {
    // Post a message to the queue
    env.QUEUE.send({
      userId: 1,
      amount: 100,
      currency: 'USD',
    })

    return new Response('Done!')
  })
])
```

when handling queues and receivng a `MessageBatch`, the `batch` contains
a collection of `messages` and the name of the `queue` which you can use to handle

```tsx title="src/worker.tsx" mark={5-9}

const app = defineApp([ /* routes... */])

export default {
  fetch: app.fetch,
  async queue(batch) {
    if (batch.queue === 'my-queue-name') {
      for (const message of batch.messages) {
        console.log('handling my-queue-name message' + JSON.stringify(message))
      }
    }
  }
} satisfies ExportedHandler<Env>;
```

> ℹ️ Note: Having a dedicated Queue for a specific message is a best practice

#### Handling Different Messages on the Same Queue

Use metadata (e.g. type, source, key) in your message body to help the consumer Worker determine where and how to retrieve the full data.

If for some reason, you decide to share a queue for different types of messages, one pattern is to set a `type` (or other attribute) in the message body to specify its purpose and how to handle its contents.

For example, when sending this payment message:

```tsx title="src/worker.tsx"
import { env } from "cloudflare:workers";

export default defineApp([
  route('/pay-with-ai', () => {
    // Post a message to the queue
    env.QUEUE.send({
      type: 'PAYMENT',
      userId: 1,
      amount: 100,
      currency: 'USD',
    })

    return new Response('Done!')
  })
])
```

One can then determine the message type and handle accordingly:

```tsx title="src/worker.tsx" mark={5-9}
const app = defineApp([ /* routes... */])

export default {
  fetch: app.fetch,
  async queue(batch) {
    for (const message of batch.messages) {
      const { type, userId, amount, currenct } = message.body as { type: string, userId: number, amount: number, currency: string };
      if (type === 'PAYMENT') {
        console.log('handling payment message' + JSON.stringify(message))
      }
    }
  }
} satisfies ExportedHandler<Env>;
```